package com.hzw.saas.service.storage.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.comparator.CompareUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.img.Img;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.text.StrSplitter;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hzw.saas.api.storage.IResourceFileService;
import com.hzw.saas.api.storage.IThumbnailService;
import com.hzw.saas.api.storage.bo.ResourceFileStorageBO;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.storage.query.ResourceFileUpdateQuery;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.RedisUtil;
import com.hzw.saas.common.storage.service.IS3FileService;
import com.hzw.saas.common.storage.util.StorageKeyUtil;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.constants.PatternConsts;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.exception.ConflictException;
import com.hzw.saas.service.storage.config.StorageConfig;
import com.hzw.saas.service.storage.mapper.ResourceFileMapper;
import com.hzw.saas.service.storage.model.ResourceFile;
import com.hzw.saas.service.storage.util.ImageUtil;
import jodd.io.FileNameUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @author zzl
 * @since 2021/12/24
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ThumbnailServiceImpl implements IThumbnailService {


    private static final String thumbnailDefaultPath = "img/thumbnail-default.png";
    private static final byte[] thumbnail;

    private final RedissonClient redissonClient;

    private final StorageConfig storageConfig;
    private final IS3FileService s3FileService;

    @Resource(name = ResourceFileServiceImpl.BEAN_NAME)
    private IResourceFileService fileService;
    @Resource
    private StorageModeHandler storageModeHandler;

    @Resource
    private ResourceFileMapper fileMapper;

    static {
        thumbnail = ResourceUtil.readBytes(thumbnailDefaultPath);
    }

    @Override
    public void getThumbnail(String pid, OutputStream ous) {
        this.getThumbnail(pid, ous, storageConfig.getThumbnailSize());
    }

    @Override
    public void getThumbnail(String pid, OutputStream ous, int size) {
        ResourceFileStorageBO fileInfo = fileService.getFileWithStorage(pid, LogicQuery.ne(ResourceStatusEnum.DELETED.groupCodes()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()), AssertEnum.EXIST);
        this.getThumbnail(fileInfo, ous, size);
    }

    @Override
    public void getThumbnail(ResourceFileStorageBO fileInfo, OutputStream ous) {
        this.getThumbnail(fileInfo, ous, storageConfig.getThumbnailSize());
    }

    @Override
    public void getThumbnail(ResourceFileStorageBO fileInfo, OutputStream ous, int size) {
        if (fileInfo == null) {
            return;
        }

        String thumbnails = fileInfo.getThumbnails();
        if (thumbnails == null) {
            thumbnails = "";
        }
        List<String> sList = StrSplitter.split(thumbnails, storageConfig.getThumbnailSizeDelimiter(), true, true);
        // 有无缩略图
        if (!sList.isEmpty()) {
            // 查找相近的缩略图
            List<Integer> thumbnailsList = sList.stream().map(Integer::parseInt).collect(Collectors.toList());
            Integer resultSize = thumbnailsList.stream()
                .min(Comparator.comparingInt(i -> Math.abs(i - size)))
                .orElse(thumbnailsList.get(0));
            String thumbnailKey = StorageKeyUtil.getThumbnailKey(fileInfo.getResourceKey(), resultSize);
            log.debug("获取缩略图，请求尺寸：{}，实际查找尺寸：{}", size, resultSize);
            if (s3FileService.exists(thumbnailKey)) {
                try {
                    s3FileService.downloadFile(thumbnailKey, ous);
                    return;
                } catch (IOException e) {
                    throw new RuntimeException(String.format("下载缩略图失败（key: %s）", thumbnailKey), e);
                }
            }
        } else {
            boolean isSmallLength = CompareUtil.compare(fileInfo.getLength(), storageConfig.getThumbnailThreshold()) <= 0;
            boolean isSmallSize = CompareUtil.compare(fileInfo.getWidth(), 0) > 0 &&
                CompareUtil.compare(fileInfo.getHeight(), 0) > 0 &&
                CompareUtil.compare(fileInfo.getWidth(), storageConfig.getThumbnailSize()) <= 0 &&
                CompareUtil.compare(fileInfo.getHeight(), storageConfig.getThumbnailSize()) <= 0;
            // 无需创建缩略图的图片
            if ((isSmallLength || isSmallSize) && s3FileService.exists(fileInfo.getResourceKey())) {
                try {
                    s3FileService.downloadFile(fileInfo.getResourceKey(), ous);
                    return;
                } catch (IOException e) {
                    throw new RuntimeException(String.format("下载缩略图失败（key: %s）", fileInfo.getResourceKey()), e);
                }
            }
        }
        // 无缩略图或未找到缩略图
        IoUtil.write(ous, false, thumbnail);
    }

    @Override
    public void saveThumbnail(String pid) {
        this.saveThumbnail(pid, storageConfig.getThumbnailSize());
    }

    @Override
    public void saveThumbnail(ResourceFileStorageBO fileInfo) {
        this.saveThumbnail(fileInfo, storageConfig.getThumbnailSize());
    }

    @Override
    public void saveThumbnail(String pid, int size) {
        ResourceFileStorageBO fileInfo = fileService.getFileWithStorage(pid, LogicQuery.ne(ResourceStatusEnum.DELETED.groupCodes()),
            LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()), AssertEnum.EXIST);
        this.saveThumbnail(fileInfo, size);
    }

    @Override
    public void saveThumbnail(ResourceFileStorageBO fileInfo, int size) {
        if (fileInfo == null) {
            return;
        }
        String thumbnailKey = StorageKeyUtil.getThumbnailKey(fileInfo.getResourceKey(), size);

        // 检查已生成的缩略图
        String thumbnails = fileInfo.getThumbnails();
        if (thumbnails == null) {
            thumbnails = "";
        }
        HashSet<String> thumbnailsSet = new HashSet<>(StrSplitter.split(thumbnails, storageConfig.getThumbnailSizeDelimiter(), true, true));

        // 已存在当前尺寸缩略图
        if (thumbnailsSet.contains(String.valueOf(size))) {
            // 检查S3上是否存在
            boolean exists = s3FileService.exists(thumbnailKey);
            if (exists) {
                log.debug("缩略图({})已存在，停止创建", thumbnailKey);
                return;
            }
        }

        // 当文件不是图片时
        String suffix = StrUtil.isBlank(fileInfo.getSuffix()) ? FileNameUtil.getExtension(fileInfo.getName()) : fileInfo.getSuffix();
        if (StrUtil.isNotBlank(suffix) && !ImageUtil.imageTypes.contains(suffix.toLowerCase())) {
            return;
        }
        if (StrUtil.isNotBlank(fileInfo.getType()) && !ImageUtil.imageTypes.contains(fileInfo.getType().toLowerCase())) {
            return;
        }

        // 图片大小小于等于缩略图阈值，直接返回原图
        if (CompareUtil.compare(fileInfo.getLength(), storageConfig.getThumbnailThreshold()) <= 0) {
            log.debug("当前文件({})长度({})小于缩略图阈值({})，停止创建缩略图", fileInfo.getResourceKey(), fileInfo.getLength(), storageConfig.getThumbnailThreshold());
            return;
        }

        // 此处文件，要么后缀为图片，要么没有后缀
        File fileData = fileService.getFileData(fileInfo.getPid(), AssertEnum.EXIST);
        String type = FileTypeUtil.getType(fileData);
        if (StrUtil.isBlank(type)) {
            type = "unknown";
        }
        Integer width = null;
        Integer height = null;
        while (true) {
            if (!ImageUtil.imageTypes.contains(type.toLowerCase())) {
                break;
            }
            Img img = Img.from(fileData);
            width = img.getImg().getWidth(null);
            height = img.getImg().getHeight(null);
            if (width <= storageConfig.getThumbnailSize() && height <= storageConfig.getThumbnailSize()) {
                log.debug("当前文件({})尺寸({})无需创建缩略图", fileInfo.getResourceKey(), width + "*" + height);
                break;
            }
            log.debug("创建图片({})缩略图({})...", fileInfo.getResourceKey(), thumbnailKey);
            File thumbnail = this.createThumbnail(fileData, size);
            // 上传至S3
            storageModeHandler.uploadToS3(thumbnailKey, thumbnail);
            // 删除缩略图本地文件
            FileUtil.del(thumbnail);
            thumbnailsSet.add(String.valueOf(size));
            break;
        }

        // 更新文件信息
        ResourceFileUpdateQuery query = new ResourceFileUpdateQuery();
        query.setPid(fileInfo.getPid());
        query.setType(type);
        query.setWidth(width);
        query.setHeight(height);
        query.setThumbnails(String.join(storageConfig.getThumbnailSizeDelimiter(), thumbnailsSet));
        fileService.updateFileByResourceKey(fileInfo.getResourceKey(), query);

    }

    @Override
    public void execThumbnailTask(Integer threadCount) {
        String taskLockName = "TASK_LOCK:generateThumbnail";
        if (tryMethodLock("METHOD_LOCK:generateThumbnail", taskLockName)) return;

        threadCount = threadCount != null && threadCount > 0 ? threadCount : 1;
        int finalThreadCount = threadCount;

        ThreadUtil.execute(() -> {
            try {
                int pageSize = 2000;
                int pageNum = 1;

                // executor
                ExecutorService executor = ThreadUtil.newExecutor(finalThreadCount);
                Queue<ResourceFile> fileQueue = new ConcurrentLinkedQueue<>();
                AtomicInteger successCount = new AtomicInteger(0);
                AtomicInteger failCount = new AtomicInteger(0);

                // log
                String taskName = "generateThumbnail";
                String logRootDir = System.getProperty("user.dir");
                String logDir = "storage-task-log";
                String today = "-" + DateUtil.format(new Date(), PatternConsts.PURE_DATETIME_PATTERN);
                String logSuffix = ".log";
                String logName = taskName + today + logSuffix;
                String errorLogName = taskName + "-error" + today + logSuffix;
                File logFile = FileUtil.file(logRootDir, logDir, logName);
                File errorLogFile = FileUtil.file(logRootDir, logDir, errorLogName);
                FileUtil.touch(logFile);
                FileUtil.touch(errorLogFile);

                String errorTemplate = "catalog: {}, resourceKey: {}, errorMsg: {}";
                List<String> errorLogList = Collections.synchronizedList(new ArrayList<>());

                long start = System.currentTimeMillis();
                while (true) {
                    IPage<ResourceFile> result = fileMapper.pagePicturesNoneThumbnails(new Page<>(pageNum, pageSize), ImageUtil.imageTypes, storageConfig.getThumbnailThreshold());
                    if (CollUtil.isEmpty(result.getRecords())) {
                        break;
                    }
                    List<ResourceFile> distinctResult = result.getRecords().stream()
                        .collect(Collectors.collectingAndThen(
                            Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(ResourceFile::getResourceKey))), ArrayList::new)
                        );
                    long stageStart = System.currentTimeMillis();
                    long total = result.getTotal();
                    long currentPageSize = result.getRecords().size();
                    fileQueue.addAll(distinctResult);
                    CountDownLatch countDownLatch = new CountDownLatch(finalThreadCount);

                    for (int i = 0; i < finalThreadCount; i++) {
                        executor.execute(() -> {
                            try {
                                ResourceFile resourceFile;
                                while ((resourceFile = fileQueue.poll()) != null) {
                                    try {
                                        this.saveThumbnail(MapperUtil.nf().map(resourceFile, ResourceFileStorageBO.class));
                                        successCount.incrementAndGet();
                                    } catch (Exception e) {
                                        String errorMsg = StrUtil.format(errorTemplate, resourceFile.getCatalog(), resourceFile.getResourceKey(), e.getMessage());
                                        log.error("[{}]创建缩略图失败, " + errorMsg, taskName);
                                        errorLogList.add(errorMsg);
                                        failCount.incrementAndGet();
                                    }
                                }
                            } finally {
                                countDownLatch.countDown();
                            }
                        });
                    }
                    try {
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        log.error("创建缩略图任务被打断");
                        throw ExceptionUtil.wrapRuntime(e);
                    }
                    long stageCost = System.currentTimeMillis() - stageStart;
                    String stageLog = StrUtil.format("[{}]创建缩略图阶段任务完成, pageNum:{}, pageSize:{}, totalSize: {}, currentTotalSize: {}, failedSize: {}, cost: {}ms", taskName, pageNum, pageSize, total, currentPageSize, errorLogList.size(), stageCost);
                    log.info(stageLog);

                    FileUtil.appendUtf8Lines(ListUtil.toList(stageLog), logFile);
                    if (!errorLogList.isEmpty()) {
                        FileUtil.appendUtf8Lines(errorLogList, errorLogFile);
                        errorLogList.clear();
                    }
                    pageNum++;
                }
                long cost = System.currentTimeMillis() - start;
                String endLog = StrUtil.format("[{}]创建缩略图任务完成，总数：{}，成功：{}, 失败：{}, 耗时: {}ms", taskName, successCount.get() + failCount.get(), successCount.get(), failCount.get(), cost);
                log.info(endLog);
                FileUtil.appendUtf8Lines(ListUtil.toList(endLog), logFile);
            } finally {
                RedisUtil.del(taskLockName);
            }
        });
    }


    // ================================================ private method ================================================ //

    private boolean tryMethodLock(String methodLockKey, String taskLockKey) {
        RLock lock = redissonClient.getLock(methodLockKey);
        if (!lock.tryLock()) {
            return true;
        }
        try {
            if (RedisUtil.hasKey(taskLockKey)) {
                throw new ConflictException("任务正在进行中...");
            } else {
                RedisUtil.set(taskLockKey, "1");
            }
        } finally {
            lock.unlock();
        }
        return false;
    }

    private File createThumbnail(File image, int size) {
        if (!FileUtil.exist(image)) {
            return null;
        }

        String thumbnailKey = StorageKeyUtil.getThumbnailKey(image.getName(), size);
        File thumbnail = new File(image.getParentFile(), thumbnailKey);

        Img img = Img.from(image);
        int width = img.getImg().getWidth(null);
        int height = img.getImg().getHeight(null);
        BigDecimal scale = this.calcScale(width, height, size);

        img.scale(scale.floatValue()).write(thumbnail);

        return thumbnail;
    }

    private BigDecimal calcScale(long width, long height, long tarSize) {
        return width > height ? NumberUtil.div(new Long(tarSize), new Long(width)) : NumberUtil.div(new Long(tarSize), new Long(height));
    }
}
